// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>

#include <fbl/function.h>
#include <fbl/string.h>
#include <fbl/unique_fd.h>
#include <fbl/vector.h>
#include <fs-management/mount.h>
#include <fs-test-utils/fixture.h>
#include <fs-test-utils/perftest.h>
#include <unittest/unittest.h>

namespace fs_test_utils {
namespace {

// File used to dump libs stdout. Allows verifying certain options.
constexpr char kFakeStdout[] = "/tmp/fake_stdout.txt";

bool ResultSetIsValid() {
  BEGIN_TEST;
  fbl::String err;
  PerformanceTestOptions p_options = PerformanceTestOptions::PerformanceTest();
  p_options.result_path = "some/path";
  ASSERT_TRUE(p_options.IsValid(&err), err.c_str());
  END_TEST;
}

bool SummaryPathSetIsValid() {
  BEGIN_TEST;
  fbl::String err;
  PerformanceTestOptions p_options = PerformanceTestOptions::PerformanceTest();
  p_options.summary_path = "some/path";
  ASSERT_TRUE(p_options.IsValid(&err), err.c_str());
  END_TEST;
}

bool PrintStatisticsSetIsValid() {
  BEGIN_TEST;
  fbl::String err;
  PerformanceTestOptions p_options = PerformanceTestOptions::PerformanceTest();
  p_options.print_statistics = true;
  ASSERT_TRUE(p_options.IsValid(&err), err.c_str());
  END_TEST;
}

bool NoOutputIsInvalid() {
  BEGIN_TEST;
  fbl::String err;
  PerformanceTestOptions p_options = PerformanceTestOptions::PerformanceTest();
  p_options.print_statistics = false;
  p_options.result_path.clear();
  p_options.summary_path.clear();

  ASSERT_FALSE(p_options.IsValid(&err), err.c_str());
  END_TEST;
}

bool InvalidOptionsReturnFalseAndPrintsUsage() {
  BEGIN_TEST;
  fbl::String err;
  char arg0[] = "/some/path/binary";
  char* argv[] = {arg0};
  PerformanceTestOptions p_options = PerformanceTestOptions::PerformanceTest();
  p_options.result_path = "some/path";
  FixtureOptions f_options = FixtureOptions::Default(DISK_FORMAT_MINFS);
  f_options.block_device_path = "some_path";
  f_options.use_ramdisk = true;

  ASSERT_FALSE(f_options.IsValid(&err));

  FILE* fp = fopen(kFakeStdout, "w");
  ASSERT_TRUE(fp);
  ASSERT_FALSE(ParseCommandLineArgs(1, argv, &f_options, &p_options, fp));
  fclose(fp);

  // Usage is printed on error.
  struct stat st;
  stat(kFakeStdout, &st);
  remove(kFakeStdout);
  ASSERT_GT(st.st_size, 0);
  END_TEST;
}

// Sanity check that we print into the stream when help option is provided.
bool HelpPrintsUsageMessage() {
  BEGIN_TEST;
  char arg0[] = "/some/path/binary";
  char arg1[] = "--help";
  char* argv[] = {arg0, arg1};
  fbl::String err;
  PerformanceTestOptions p_options = PerformanceTestOptions::PerformanceTest();
  FixtureOptions f_options = FixtureOptions::Default(DISK_FORMAT_MINFS);

  FILE* fp = fopen(kFakeStdout, "w");
  ASSERT_TRUE(fp);
  ASSERT_FALSE(ParseCommandLineArgs(2, argv, &f_options, &p_options, fp));
  fclose(fp);

  struct stat st;
  stat(kFakeStdout, &st);
  remove(kFakeStdout);
  ASSERT_GT(st.st_size, 0);
  END_TEST;
}

// Verifies that ParseCommandLineArgs actually sets the respective fields in the
// option structs.
bool OptionsAreOverwritten() {
  BEGIN_TEST;
  fbl::Vector<fbl::String> argvs = {
      "/some/binary",
      "-p",
      "--use_fvm",
      "--fvm_slice_size",
      "8192",
      "--use_ramdisk",
      "--ramdisk_block_size",
      "1024",
      "--ramdisk_block_count",
      "500",
      "--runs",
      "4",
      "--out",
      "some_path",
      "--summary_path",
      "other_path",
      "--print_statistics",
      "--fs",
      "blobfs",
  };
  const char* argv[argvs.size() + 1];
  for (size_t i = 0; i < argvs.size(); ++i) {
    argv[i] = argvs[i].data();
  }
  argv[argvs.size()] = nullptr;

  fbl::String err;
  PerformanceTestOptions p_options = PerformanceTestOptions::PerformanceTest();
  FixtureOptions f_options = FixtureOptions::Default(DISK_FORMAT_MINFS);

  FILE* fp = fopen(kFakeStdout, "w");
  ASSERT_TRUE(fp);
  ASSERT_TRUE(
      ParseCommandLineArgs(static_cast<int>(argvs.size()), argv, &f_options, &p_options, fp));
  fclose(fp);

  // Usage is not logged.
  struct stat st;
  stat(kFakeStdout, &st);
  remove(kFakeStdout);
  ASSERT_EQ(st.st_size, 0);

  // Parameters parsed.
  ASSERT_TRUE(f_options.block_device_path == "");
  ASSERT_TRUE(f_options.use_ramdisk);
  ASSERT_EQ(f_options.ramdisk_block_size, 1024);
  ASSERT_EQ(f_options.ramdisk_block_count, 500);
  ASSERT_TRUE(f_options.use_fvm);
  ASSERT_EQ(f_options.fvm_slice_size, 8192);
  ASSERT_EQ(f_options.fs_type, DISK_FORMAT_BLOBFS);

  ASSERT_FALSE(p_options.is_unittest);
  ASSERT_TRUE(p_options.result_path == "some_path");
  ASSERT_TRUE(p_options.summary_path == "other_path");
  ASSERT_TRUE(p_options.print_statistics);
  ASSERT_EQ(p_options.sample_count, 4);

  END_TEST;
}

bool RunTestCasesPreservesOrder() {
  BEGIN_TEST;
  PerformanceTestOptions p_options = PerformanceTestOptions::UnitTest();
  FixtureOptions f_options = FixtureOptions::Default(DISK_FORMAT_MINFS);
  f_options.isolated_devmgr = true;
  p_options.sample_count = 1;
  fbl::Vector<int> calls;

  auto test_1 = [&calls](perftest::RepeatState* state, Fixture* fixture) {
    state->DeclareStep("test_1");
    while (state->KeepRunning()) {
      calls.push_back(1);
    }
    return true;
  };
  auto test_2 = [&calls](perftest::RepeatState* state, Fixture* fixture) {
    state->DeclareStep("test_2");
    while (state->KeepRunning()) {
      calls.push_back(2);
    }
    return true;
  };
  auto test_3 = [&calls](perftest::RepeatState* state, Fixture* fixture) {
    state->DeclareStep("test_3");
    while (state->KeepRunning()) {
      calls.push_back(3);
    }
    return true;
  };

  TestCaseInfo info;
  info.name = "MyTestCase";
  info.tests.push_back({std::move(test_1), "test_1", /*required_disk_space=*/0});
  info.tests.push_back({std::move(test_2), "test_2", 0});
  info.tests.push_back({std::move(test_3), "test_3", 0});
  info.teardown = false;

  fbl::Vector<TestCaseInfo> test_cases;
  test_cases.push_back(std::move(info));
  ASSERT_TRUE(RunTestCases(f_options, p_options, test_cases, /*out=*/nullptr));

  // Verify order is preserved.
  ASSERT_EQ(calls.size(), 3);
  ASSERT_EQ(calls[0], 1);
  ASSERT_EQ(calls[1], 2);
  ASSERT_EQ(calls[2], 3);

  END_TEST;
}

bool RunTestCasesPreservesOrderWithMultipleSamples() {
  BEGIN_TEST;
  PerformanceTestOptions p_options = PerformanceTestOptions::UnitTest();
  FixtureOptions f_options = FixtureOptions::Default(DISK_FORMAT_MINFS);
  f_options.isolated_devmgr = true;
  p_options.is_unittest = false;
  p_options.sample_count = 10;
  fbl::Vector<int> calls;

  auto test_1 = [&calls](perftest::RepeatState* state, Fixture* fixture) {
    state->DeclareStep("test_1");
    while (state->KeepRunning()) {
      calls.push_back(1);
    }
    return true;
  };
  auto test_2 = [&calls](perftest::RepeatState* state, Fixture* fixture) {
    state->DeclareStep("test_2");
    while (state->KeepRunning()) {
      calls.push_back(2);
    }
    return true;
  };
  auto test_3 = [&calls](perftest::RepeatState* state, Fixture* fixture) {
    state->DeclareStep("test_3");
    while (state->KeepRunning()) {
      calls.push_back(3);
    }
    return true;
  };

  TestCaseInfo info;
  info.sample_count = 20;
  info.name = "MyTestCase";
  info.tests.push_back({std::move(test_1), "test_1", /*required_disk_space=*/0});
  info.tests.push_back({std::move(test_2), "test_2", 0});
  info.tests.push_back({std::move(test_3), "test_3", 0});
  info.teardown = false;

  fbl::Vector<TestCaseInfo> test_cases;
  test_cases.push_back(std::move(info));
  ASSERT_TRUE(RunTestCases(f_options, p_options, test_cases, /*out=*/nullptr));

  // Verify order is preserved.
  ASSERT_EQ(calls.size(), 60);
  for (int i = 0; i < 20; ++i) {
    ASSERT_EQ(calls[i], 1);
    ASSERT_EQ(calls[(20 + i)], 2);
    ASSERT_EQ(calls[(40 + i)], 3);
  }

  END_TEST;
}

bool RunTestCasesWritesResultsAndStatistics() {
  BEGIN_TEST;
  PerformanceTestOptions p_options = PerformanceTestOptions::PerformanceTest();
  p_options.result_path = "/data/results.json";
  p_options.summary_path = "/data/summary.txt";
  p_options.print_statistics = true;

  FixtureOptions f_options = FixtureOptions::Default(DISK_FORMAT_MINFS);
  f_options.isolated_devmgr = true;
  p_options.sample_count = 1;

  auto test_1 = [](perftest::RepeatState* state, Fixture* fixture) {
    state->DeclareStep("test_1");
    state->DeclareStep("test_2");
    while (state->KeepRunning()) {
      state->NextStep();
    }
    return true;
  };

  TestCaseInfo info;
  info.name = "MyTestCase";
  info.tests.push_back({std::move(test_1), "test_1", /*required_disk_space=*/0});
  info.teardown = false;

  fbl::Vector<TestCaseInfo> test_cases;
  test_cases.push_back(std::move(info));

  FILE* fp = fopen(kFakeStdout, "w+");
  ASSERT_TRUE(fp);
  ASSERT_TRUE(RunTestCases(f_options, p_options, test_cases, fp));
  fseek(fp, 0, SEEK_SET);
  // Look for test_1.test_1 in fake_std.txt (test_name.step_name).
  char* buffer = nullptr;
  size_t length = 0;
  ssize_t read;
  bool found_1 = false;
  bool found_2 = false;
  while ((read = getline(&buffer, &length, fp)) != -1 && (!found_1 || !found_2)) {
    if (strstr(buffer, "test_1.test_1")) {
      found_1 = true;
    } else if (strstr(buffer, "test_1.test_2")) {
      found_2 = true;
    }
    free(buffer);
    buffer = nullptr;
    length = 0;
  }
  free(buffer);
  buffer = nullptr;
  remove(kFakeStdout);
  fclose(fp);
  EXPECT_TRUE(found_1);
  EXPECT_TRUE(found_2);

  struct stat st;
  stat(p_options.result_path.c_str(), &st);
  remove(p_options.result_path.c_str());
  EXPECT_GT(st.st_size, 0);

  stat(p_options.summary_path.c_str(), &st);
  remove(p_options.summary_path.c_str());
  EXPECT_GT(st.st_size, 0);

  END_TEST;
}

BEGIN_TEST_CASE(FsPerformanceTestOptions)
RUN_TEST(ResultSetIsValid)
RUN_TEST(SummaryPathSetIsValid)
RUN_TEST(PrintStatisticsSetIsValid)
RUN_TEST(NoOutputIsInvalid)
END_TEST_CASE(FsPerformanceTestOptions)

BEGIN_TEST_CASE(FsPerformanceTestLib)
RUN_TEST(InvalidOptionsReturnFalseAndPrintsUsage)
RUN_TEST(OptionsAreOverwritten)
RUN_TEST(HelpPrintsUsageMessage)
RUN_TEST(RunTestCasesPreservesOrder)
RUN_TEST(RunTestCasesPreservesOrderWithMultipleSamples)
RUN_TEST(RunTestCasesWritesResultsAndStatistics)
END_TEST_CASE(FsPerformanceTestLib)

}  // namespace
}  // namespace fs_test_utils
